/** * This library parses a chunk of text and attempts to force it to confirm to * a specific syntax. The syntax is used to describe what work was accomplished * during the day: * =============================================== * 1. Worked on the authentication page * - Built a form for authentication * - Implemented a model to authenticate users * 2. Worked on the reset password page * - Added a feature to send an email to the user * =============================================== */ // Utiliy extension for strings to strip leading spaces String.prototype.ltrimSpace = function() { return this.replace(/^ +/g,""); } // Utility extension for strings to strip leading and trailing whitespace String.prototype.trim = function() { return this.replace(/^\s+|\s+$/g,""); } // Dynamically reformats a piece of text according to the business rules for // time logs // @param string text The text to be reformatted function reformat_body(text){ text = format_token_rule(text); // (the live system contains additional rules here written by other developers) return text; } // Reformats the structure of a piece of text to match the required structure // for time logs // @param string text The text to be reformatted function format_token_rule(text) { // Contains text after it has been split token strings var tokens = []; // Breaks the input string up by numbers, periods, dashes and new-lines // (these are the significant characters in the time log format) var tokenizer = new jQuery.tokenizer( [ /[0-9]+/, '.', '-', "\n" ], function(text, isSep) { tokens.push(text); } ); tokenizer.parse(text.trim().replace("\r", '')); // The output var newtext = ''; // A state machine to validate and correct the tokenized input var state = 'start'; // The current state var curcount = 0; // Number of lines in the time log var pos = 0; // Current position within the token array var limit = 1000; // Maximum number of iterations (safeguard against infinite loops) var indented = false; // Indicates whether the current line is indented // Iterate through the tokens in the input string while(pos < tokens.length) { // Safe-guard against infinite loops limit--; if(limit <= 0) { break; } // Fetch the token to parse for this iteration var token = tokens[pos]; // Take action depending upon the current state if(state == 'start') { // start: The start of a numbered work item curcount++; // Move this piece of text into the output newtext += curcount; // Advance to the next state state = 'poststart'; // Work items are supposed to be numbered; only advance to the next // token if that is the case. if(token.match(/^[0-9]+$/)) { pos++; } } else if(state == 'poststart') { // poststart: After the number in a work item, they are supposed to // have a single dot. newtext += '.'; state = 'postdot'; // Only advance to the next token if this one is correct (ie: a dot) if(token == '.') { pos++; } } else if(state == 'postdot') { // postdot: A space should follow the dot newtext += ' '; state = 'worktask'; // Only advance to the next token if this one is correct (ie: a space) if(token == ' ') { pos++; } } else if(state == 'worktask') { // worktask: After the space, the log must contain a single line of // arbitrary text. This state check for the *start* of that line. // See if we are at the end of the line (this would be an invalid line) if(token == "\n") { // Yes newtext += "\n"; state = 'postworktask'; } else { // The start of the line should not be a dot or dash // If it does, stay in the current state and advance until we find one if(token != "." && token != "-") { // The start of the state must start with a letter or number token = token.replace(/^[^A-z0-9]+/, ''); if(token.length > 0) { // This is a valid start of a work item. Output it and // make sure it's uppercase newtext += token.charAt(0).toUpperCase() + token.slice(1); state = 'inworktask'; } else { // This is an invalid start of line, try to start over with // the next line state = 'postworktask'; } } } pos++; } else if(state == 'inworktask') { // inworktask: After the start but before the end of a line of // arbitrary text. // See if we have reached the end of the line if(token == "\n") { // Yes, advance to the next line newtext += "\n"; state = 'postworktask'; } else { // No, continue appending this arbitrary text to the output and // stay in this state. newtext += token; } pos++; } else if(state == 'postworktask') { // postworktask: after the end of the line of arbitrary text if(token.match(/^ +$/)) { // This line is indented (the first line should never be indented) indented = true; pos++; } else if(token.match(/^[0-9]/) || token.match(/^( +)?#/)) { // This line starts with a number or number sign, assume it is // a numbered work item. state = 'start'; indented = false; } else if(token.match(/^( +)?[A-z]/)) { // This line starts with text, but neither a number nor a dash // (an incorrect format). Guess how to treat it based on // whether it is indented or not. if(token.charAt(0) == ' ' || indented) { // Assume this is a work detail line (dashed) state = 'startworkdetail'; } else { // Assume this is a work item line (numbered) state = 'start'; } indented = false; } else { // Assume the start of a work detail line (dashed) state = 'startworkdetail'; indented = false; } } else if(state == 'startworkdetail') { // startworkdetail: the start of a dashed line of work detail // information (should start with a space, dash, space) if(token == '-') { pos++; newtext += ' - '; state = 'workdetail'; } else { // Leading dash is missing, add it and don't advance the token newtext += ' - '; state = 'workdetail'; } } else if(state == 'workdetail') { // workdetail: start of the arbitrary text of a work detail line (dashed line) if(token == "\n") { // End of line newtext = newtext.substr(0, newtext.length-3); state = 'postworktask'; } else { // The text should not lead with a dot or dash if(token != "." && token != "-") { // The text must lead with a letter or number token = token.replace(/^[^A-z0-9]+/, ''); if(token.length > 0) { // Add to the output and continue reading the line newtext += token.charAt(0).toUpperCase() + token.slice(1); state = 'inworkdetail'; } else { // Error on this line, go to the next newtext = newtext.substr(0, newtext.length-3); state = 'postworktask'; } } } pos++; } else if(state == 'inworkdetail') { // inworkdetail: within the line of text describing a work item if(token == "\n") { // End of line newtext += "\n"; state = 'postworktask'; } else { // Append to output newtext += token; } pos++; } } // Edge case: if there is only one line-item, don't include a number if(curcount == 1) { newtext = newtext.slice(3); } return newtext; }